package com.up.mongo.core;


import com.mongodb.BasicDBObject;
import com.mongodb.bulk.BulkWriteInsert;
import com.mongodb.bulk.BulkWriteResult;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.model.*;
import com.up.mongo.utils.MongoDBUtils;
import org.bson.BsonValue;
import org.bson.Document;
import org.bson.json.JsonMode;
import org.bson.json.JsonWriterSettings;
import org.bson.json.StrictJsonWriter;
import org.bson.types.ObjectId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.annotation.Id;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.BulkOperations;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.data.util.Pair;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @author: baiMl
 * @title: IBaseMongoService
 * @projectName: 海联技术中台V2.0
 * @depart 软件事业部
 * @description:
 * @date: 9/14/2022 3:12 PM(Wednesday)
 */
public abstract class BaseMongoService<E, ID> implements IBaseMongoRepository<E, ID> {
    private final Logger log = LoggerFactory.getLogger(BaseMongoService.class);

    private Class<E> entityClass;

    @Resource
    protected MongoTemplate mongoTemplate;

    private final static JsonWriterSettings settings =
            JsonWriterSettings.builder()
                    .outputMode(JsonMode.EXTENDED)
                    .int64Converter((Long value, StrictJsonWriter writer) ->writer.writeString(Long.toString(value)))
                    .int32Converter((Integer value, StrictJsonWriter writer) ->writer.writeNumber(Integer.toString(value)))
                    .doubleConverter((Double value,StrictJsonWriter writer)->writer.writeNumber(Double.toString(value)))
                    .objectIdConverter((ObjectId value, StrictJsonWriter writer)->writer.writeString(value.toHexString()))
                    .dateTimeConverter((Long value, StrictJsonWriter writer)->writer.writeString(MongoDBUtils.longToStrDate(value,MongoDBUtils.DATE_FMT_4)))
                    .build();
    @Override
    public E save(E entity) {
        return mongoTemplate.save(entity);
    }

    public void batchSave(List<E> list){
        mongoTemplate.insertAll(list);
    }

    @Override
    public void deleteById(ID id) {
        Query query = new Query();
        query.addCriteria(Criteria.where(idFieldName).is(id));
        mongoTemplate.remove(query, getEntityClass());
    }

    @Override
    public void deleteByQuery(Query query) {
        mongoTemplate.remove(query,getEntityClass());
    }

    @Override
    public void deleteByCondition(E t) {
        Query query = MongoDBUtils.buildBaseQuery(t);
        mongoTemplate.remove(query, getEntityClass());
    }

    @Override
    public void updateById(ID id, E t) {
        Query query = new Query();
        query.addCriteria(Criteria.where(idFieldName).is(id));
        Update update = buildBaseUpdate(t,false);
        updateFirst(query, update);
    }

    @Override
    public void updateById(ID id, E t,boolean updateAll) {
        Query query = new Query();
        query.addCriteria(Criteria.where(idFieldName).is(id));
        Update update = buildBaseUpdate(t,updateAll);
        updateFirst(query, update);
    }

    @Override
    public List<E> findByCondition(E t) {
        Query query = MongoDBUtils.buildBaseQuery(t);
        return mongoTemplate.find(query, getEntityClass());
    }

    @Override
    public List<E> findByCondition(E t, String collectionName) {
        Query query = MongoDBUtils.buildBaseQuery(t);
        return mongoTemplate.find(query, getEntityClass(), collectionName);
    }

    @Override
    public List<E> find(Query query) {
        return mongoTemplate.find(query, this.getEntityClass());
    }

    @Override
    public List<E> find(Query query, String collectionName) {
        if(StringUtils.isEmpty(collectionName)) {
            return mongoTemplate.find(query, this.getEntityClass());
        }
        return mongoTemplate.find(query, this.getEntityClass(), collectionName);
    }

    @Override
    public E findOne(Query query) {
        return mongoTemplate.findOne(query, this.getEntityClass());
    }

    @Override
    public E findOne(E t) {
        Query query = MongoDBUtils.buildBaseQuery(t);
        return findOne(query);
    }

    @Override
    public void updateMulti(Query query, Update update) {
        mongoTemplate.updateMulti(query, update, this.getEntityClass());
    }

    @Override
    public void updateById(ID id, Update update) {
        Query query = new Query();
        query.addCriteria(Criteria.where(idFieldName).is(id));
        updateFirst(query, update);
    }

    @Override
    public void updateFirst(Query query, Update update) {
        mongoTemplate.updateFirst(query, update, this.getEntityClass());
    }
    @Override
    public void upsert(Query query, Update update) {
        mongoTemplate.upsert(query, update, this.getEntityClass());
    }

    @Override
    public E findById(ID id) {
        Class<E> entityClass = this.getEntityClass();
        //logQuery( new Query(Criteria.where("id").is(id)));
        return mongoTemplate.findById(id, entityClass);
    }

    @Override
    public E findById(ID id, String collectionName) {
        return mongoTemplate.findById(id, this.getEntityClass(), collectionName);
    }

    @Override
    public Page<E> findPage(Page<E> page, Query query) {
        return findPage(page, query, null);
    }

    @Override
    public Page<E> findPage(Page<E> page, Query query, String collectionName) {
        //如果没有条件 则所有全部
        query = query == null ? new Query(Criteria.where(idFieldName).exists(true)) : query;
        long count = StringUtils.isEmpty(collectionName) ? this.count(query) : this.count(query, collectionName);
        // 总数
        page.setTotal(count);
        long currentPage = page.getCurrent();
        long pageSize = page.getSize();
        query.skip((currentPage - 1) * pageSize).limit(Integer.parseInt(pageSize+""));
        List<E> rows = this.find(query, collectionName);
        page.build(rows);
        return page;
    }

    @Override
    public Page<E> findPageByCondition(Page<E> page, E t) {
        Query query = MongoDBUtils.buildBaseQuery(t);
        return findPage(page, query);
    }

    @Override
    public Page<E> findPageByCondition(Page<E> page, E t, String collectionName) {
        Query query = MongoDBUtils.buildBaseQuery(t);
        return findPage(page, query, collectionName);
    }

    @Override
    public long count(Query query) {
        return mongoTemplate.count(query, this.getEntityClass());
    }

    @Override
    public long count(Query query, String collectionName) {
        return mongoTemplate.count(query, collectionName);
    }

    @Override
    public E findAndReplace(ID id, E entity) {
        Query query = new Query();
        query.addCriteria(Criteria.where(idFieldName).is(id));
        return mongoTemplate.findAndReplace(query, entity);
    }

    @Override
    public E findAndReplace(Query query, E entity) {
        return mongoTemplate.findAndReplace(query, entity);
    }

    @Override
    public void batchUpsert(List<E> data) {
        if (data == null || data.isEmpty()) {
            return;
        }
        List<Pair<Query, Update>> updateList = new ArrayList<>(data.size());
        BulkOperations operations = mongoTemplate.bulkOps(BulkOperations.BulkMode.UNORDERED, getEntityClass());
        for (E datum : data) {
            Document doc = new Document();
            mongoTemplate.getConverter().write(datum, doc);
            Update update = Update.fromDocument(new Document("$set", doc));
            try {
                Query query = new Query(new
                        Criteria(idFieldName).is(getIdField(datum)));
                Pair<Query, Update> updatePair = Pair.of(query, update);
                updateList.add(updatePair);
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        }
        operations.upsert(updateList);
        operations.execute();
    }
    @Override
    public void addOrUpdate(List<E> dataList) {
        if(CollectionUtils.isEmpty(dataList)){
            return;
        }
        try{
            MongoCollection<Document> collection = mongoTemplate.getCollection(mongoTemplate.getCollectionName(getEntityClass()));

            List<WriteModel<Document>> list = new ArrayList<>();
            Iterator<E> iterator = dataList.iterator();
            E eum = null;
            while (iterator.hasNext()) {
                eum = iterator.next();
                Document doc = new Document();
                mongoTemplate.getConverter().write(eum, doc);
                ID id = getIdField(eum);

                if(id!= null){
                    doc.remove("_id");
                    BasicDBObject update = new BasicDBObject().append("$set", doc);
                    UpdateOneModel<Document> um = null;
                    /**
                     * 判断是否为自动生成的ID
                     */
                    if(ObjectId.isValid(id.toString())){
                        //是 则需要用ObjectId进行匹配
                        um = new UpdateOneModel<Document>(Filters.eq(new ObjectId(id.toString())), update);
                    }else{
                        //否 则使用数据本身对象ID进行匹配
                        um = new UpdateOneModel<Document>(Filters.eq(id), update);
                    }
                    list.add(um);
                }else{
                    InsertOneModel insertOneModel = new InsertOneModel(doc);
                    list.add(insertOneModel);
                }
            }
            BulkWriteResult bulkWriteResult = collection.bulkWrite(list, new BulkWriteOptions().ordered(true));
            /**
             * 处理数据将ID回写到数据中
             */
            Map<Integer,BsonValue> integerObjectMap = bulkWriteResult.getInserts().stream().collect(Collectors.toMap(BulkWriteInsert::getIndex, BulkWriteInsert::getId));
            Object o = null;
            BsonValue bsonValue = null;
            for(int i = 0 ;i<dataList.size();i++){
                bsonValue = integerObjectMap.get(i);
                if(bsonValue==null){
                    continue;
                }
                o=dataList.get(i);
                try {
                    Field field = getEntityClass().getDeclaredField(idFieldName);
                    field.setAccessible(true);
                    field.set(o,bsonValue.asObjectId().getValue().toString());
                } catch (NoSuchFieldException e) {
                    e.printStackTrace();
                }
            }
        }catch (Exception e){
            e.printStackTrace();
            throw new RuntimeException(entityClass.getName() + ":批量新增或修改数据失败");
        }
    }

    @Override
    public MongoTemplate getMongoTemplate() {
        return mongoTemplate;
    }

    protected Class<E> getEntityClass() {
        if (entityClass == null) {
            entityClass = getSuperClassGenricType(getClass(), 0);
        }
        setIdField();
        return entityClass;
    }

    private String idFieldName;
    private Field idField;

    private void setIdField() {
        if (idField == null) {
            for (Field declaredField : entityClass.getDeclaredFields()) {
                if (declaredField.isAnnotationPresent(Id.class)) {
                    idFieldName = declaredField.getName();
                    idField = declaredField;
                    idField.setAccessible(true);
                    break;
                }
            }
        }
    }

    private ID getIdField(E datum) throws IllegalAccessException {
        if (null == idField) {
            throw new RuntimeException(entityClass.getName() + ":必要要设置@Id,否则无法进行批量更新");
        }
        Object idObj = idField.get(datum);
        if (idObj == null) {
            return null;
            //throw new RuntimeException(entityClass.getName() + ":根据ID批量更新，要确保ID必须不为空");
        }
        return (ID) idObj;
    }

    /**
     * 根据vo构建更新条件Query
     *
     * @param t
     * @return
     */
    private Update buildBaseUpdate(E t,boolean updateAll) {
        Update update = new Update();
        Field[] fields = t.getClass().getDeclaredFields();
        for (Field field : fields) {
            if (field.isAnnotationPresent(Id.class)) {
                continue;
            }
            field.setAccessible(true);
            try {
                String fieldName = field.getName();
                Object value = field.get(t);
                if(updateAll){
                    update.set(fieldName, value);
                }else{
                    if (value != null) {
                        if (ObjectUtils.isArray(value)) {
                            Object[] data = (Object[]) value;
                            update.addToSet(fieldName).each(data);
                        } else if (value instanceof List) {
                            List<?> data = (List<?>) value;
                            update.addToSet(fieldName).each(data);
                        } else {
                            update.set(fieldName, value);
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return update;
    }

    /**
     * 根据vo构建查询条件Query
     *
     * @return
     */


    private Class getSuperClassGenricType(final Class clazz, final int index) {
        Type genType = clazz.getGenericSuperclass();

        if (!(genType instanceof ParameterizedType)) {
            log.warn(clazz.getSimpleName() + "'s superclass not ParameterizedType");
            return Object.class;
        }

        Type[] params = ((ParameterizedType) genType).getActualTypeArguments();

        if (index >= params.length || index < 0) {
            log.warn("Index: " + index + ", Size of " + clazz.getSimpleName() + "'s Parameterized Type: "
                    + params.length);
            return Object.class;
        }
        if (!(params[index] instanceof Class)) {
            log.warn(clazz.getSimpleName() + " not set the actual class on superclass generic parameter");
            return Object.class;
        }

        return (Class) params[index];
    }

    @Override
    public void setMongoTemplate(MongoTemplate mongoTemplate) {
        this.mongoTemplate = mongoTemplate;
    }

    @Override
    public void setTargetClass(Class<?> targetClass) {
        this.entityClass = (Class<E>) targetClass;
        setIdField();
    }
    @Override
    public JsonWriterSettings getJsonWriterSettings(){
        return settings;
    }

    /**
     * 查询Mongo分页数据
     * @param target
     * @param pageIndex
     * @param pageSize
     * @param query
     * @param orders
     * @param <T>
     * @return
     */
    public <T> PageInfo<T> queryPage(Class<T> target, Integer pageIndex, Integer pageSize, Query query, Sort.Order... orders){
        //创建分页查询对象
        Pageable pageable = PageRequest.of(pageIndex- 1, pageSize);
        return queryPage(target,this.mongoTemplate,pageable,query,orders);
    }

    /**
     * 查询Mongo分页数据
     * @param target
     * @param mongoTemplate
     * @param pageIndex
     * @param pageSize
     * @param query
     * @param orders
     * @param <T>
     * @return
     */
    public <T> PageInfo<T> queryPage(Class<T> target, MongoTemplate mongoTemplate, Integer pageIndex, Integer pageSize, Query query, Sort.Order... orders){
        //创建分页查询对象
        Pageable pageable = PageRequest.of(pageIndex- 1, pageSize);
        return queryPage(target,mongoTemplate,pageable,query,orders);
    }

    /**
     * 查询Mongo分页数据
     * @param target
     * @param pageable
     * @param query
     * @param orders
     * @param <T>
     * @return
     */
    public <T> PageInfo<T> queryPage(Class<T> target, Pageable pageable, Query query, Sort.Order... orders){
        return queryPage(target,this.mongoTemplate,pageable,query,orders);
    }

    /**
     * 查询Mongo分页数据
     * @param target
     * @param mongoTemplate
     * @param pageable
     * @param query
     * @param orders
     * @param <T>
     * @return
     */
    public <T> PageInfo<T> queryPage(Class<T> target, MongoTemplate mongoTemplate,Pageable pageable, Query query, Sort.Order... orders){
        //获取总数量 不要排序 影响性能
        Long count = mongoTemplate.count(query, target);
        query.with(pageable);
        //排序
        //创建排序规则
        Sort by = Sort.by(orders);
        query.with(by);
        List<T> datasources = mongoTemplate.find(query, target);
        return convertListToPage(datasources,count,pageable.getPageNumber(),pageable.getPageSize());
    }

    /**
     * 将集合对象转换为分页对象
     * @param rows
     * @param count
     * @param pageIndex
     * @param pageSize
     * @param <T>
     * @return
     */
    private <T> PageInfo<T> convertListToPage(List<T> rows,Long count, Integer pageIndex, Integer pageSize){
        PageInfo<T> pageInfo = new PageInfo<>();
        pageInfo.setTotalList(rows);
        pageInfo.setTotal(Math.toIntExact(count));
        pageInfo.setCurrentPage(pageIndex+1);
        pageInfo.setPageSize(pageSize);
        pageInfo.setStart(pageIndex * pageSize);
        if (count % pageSize == 0) {
            pageInfo.setTotalPage((int) (count / pageSize));
        } else {
            pageInfo.setTotalPage((int) (count / pageSize) + 1);
        }
        return pageInfo;
    }

    @Override
    public Boolean isExists(Query query) {
        return mongoTemplate.exists(query,getEntityClass());
    }

    @Override
    public Boolean isExists(ID id) {
        Query query = new Query();
        query.addCriteria(Criteria.where(idFieldName).is(id));
        return mongoTemplate.exists(query,getEntityClass());
    }
}
